/*
 * Copyright (C) 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.gxp.compiler;

import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gxp.compiler.alerts.AlertPolicy;
import com.google.gxp.compiler.alerts.AlertSet;
import com.google.gxp.compiler.alerts.AlertSetBuilder;
import com.google.gxp.compiler.alerts.AlertSink;
import com.google.gxp.compiler.alerts.UniquifyingAlertSink;
import com.google.gxp.compiler.alerts.common.IOError;
import com.google.gxp.compiler.base.OutputLanguage;
import com.google.gxp.compiler.codegen.CodeGeneratorFactory;
import com.google.gxp.compiler.depend.DependencyGraph;
import com.google.gxp.compiler.dot.DotWriter;
import com.google.gxp.compiler.dot.GraphRenderer;
import com.google.gxp.compiler.dot.ReflectiveGraphRenderer;
import com.google.gxp.compiler.fs.FileRef;
import com.google.gxp.compiler.parser.Parser;
import com.google.gxp.compiler.parser.SaxXmlParser;
import com.google.gxp.compiler.parser.SourceEntityResolver;
import com.google.gxp.compiler.schema.BuiltinSchemaFactory;
import com.google.gxp.compiler.schema.DelegatingSchemaFactory;
import com.google.gxp.compiler.schema.FileBackedSchemaFactory;
import com.google.gxp.compiler.schema.SchemaFactory;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Writer;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * The core GXP Compiler class.  Takes a {@link Configuration} and can then
 * be called to execute a compile cycle.
 */
public class Compiler {
  private final ImmutableSet<FileRef> sourceFiles;
  private final ImmutableSet<FileRef> schemaFiles;
  private final ImmutableSet<OutputLanguage> outputLanguages;
  private final long compilationVersion;
  private final CodeGeneratorFactory codeGeneratorFactory;
  private final ImmutableSet<FileRef> allowedOutputs;
  private final FileRef dependencyFile;
  private final FileRef propertiesFile;
  private final AlertPolicy alertPolicy;
  private final ImmutableSet<Phase> dotPhases;
  private final SourceEntityResolver entityResolver;

  public Compiler(Configuration config) throws  InvalidConfigException {
    // TODO(laurence): use config.isDebugEnabled()
    validateAllowedOutputs(config.getAllowedOutputFiles(),
                           config.getSourceFiles(),
                           config.getOutputLanguages(),
                           config.getCompilationVersion());

    sourceFiles = ImmutableSet.copyOf(config.getSourceFiles());
    schemaFiles = ImmutableSet.copyOf(config.getSchemaFiles());
    outputLanguages = ImmutableSet.copyOf(config.getOutputLanguages());
    compilationVersion = config.getCompilationVersion();
    codeGeneratorFactory = config.getCodeGeneratorFactory();
    allowedOutputs = ImmutableSet.copyOf(config.getAllowedOutputFiles());
    dependencyFile = config.getDependencyFile();
    propertiesFile = config.getPropertiesFile();
    alertPolicy = config.getAlertPolicy();
    dotPhases = ImmutableSet.copyOf(config.getDotPhases());
    entityResolver = config.getEntityResolver();
  }

  /**
   * Executes compilation and returns the @code Alert}s generated by
   * compile as an {@link AlertSet}
   */
  public AlertSet call() {
    AlertSetBuilder alertSetBuilder = new AlertSetBuilder();
    call(alertSetBuilder);
    return alertSetBuilder.buildAndClear();
  }

  /**
   * Executes compilation and passes the {@code Alert}s generated by
   * compile to the {@link AlertSink}
   */
  public void call(AlertSink alertSink) {
    // Make sure that any given alert is only sent to the sink once
    alertSink = new UniquifyingAlertSink(alertSink);

    // build up a schema factory
    SchemaFactory schemaFactory = new DelegatingSchemaFactory(
        new FileBackedSchemaFactory(alertSink, schemaFiles),
        new BuiltinSchemaFactory(alertSink));

    Parser parser = new Parser(schemaFactory, SaxXmlParser.INSTANCE, entityResolver);
    CompilationManager manager = readCompilationManager();
    CompilationSet.Builder compilationSetBuilder =
        new CompilationSet.Builder(parser, codeGeneratorFactory, manager)
                .setCompilationVersion(compilationVersion)
                .setPropertiesFile(propertiesFile);
    CompilationSet compilationSet = compilationSetBuilder.build(sourceFiles);

    Predicate<FileRef> shouldCompileFilePredicate = allowedOutputs.isEmpty()
        ? Predicates.<FileRef>alwaysTrue()
        : Predicates.<FileRef>in(allowedOutputs);

    compilationSet.compile(alertSink, alertPolicy, outputLanguages, shouldCompileFilePredicate);

    writeDotFiles(compilationSet, alertSink);
    writeCompilationManager(new DependencyGraph(compilationSet));
  }

  private void writeDotFiles(CompilationSet compilationSet, AlertSink alertSink) {
    List<CompilationUnit> compilationUnits = compilationSet.getCompilationUnits();
    int i = 0;
    for (Phase phase : Phase.values()) {
      i++;
      if (dotPhases.contains(phase)) {
        String suffix = String.format(".%02d.%s.dot", i,
                                      phase.name().toLowerCase().replace("_", "-"));
        for (CompilationUnit compilationUnit : compilationUnits) {
          FileRef fileRef = compilationUnit.getSourceFileRef().removeExtension().addSuffix(suffix);
          try {
            Writer writer = fileRef.openWriter(Charsets.US_ASCII);
            try {
              DotWriter out = new DotWriter(writer);
              GraphRenderer<Object> renderer =
                  new ReflectiveGraphRenderer(phase.name().toLowerCase());
              renderer.renderGraph(out, phase.getForest(compilationUnit).getChildren());
            } finally {
              writer.close();
            }
          } catch (IOException iox) {
            alertSink.add(new IOError(fileRef, iox));
          }
        }
      }
    }
  }

  private CompilationManager readCompilationManager() {
    CompilationManager manager = SimpleCompilationManager.INSTANCE;

    if (dependencyFile != null) {
      try {
        ObjectInputStream ois = new ObjectInputStream(dependencyFile.openInputStream());
        Object read = ois.readObject();
        if (read instanceof CompilationManager) {
          manager = (CompilationManager) read;
        }
        ois.close();
      } catch (Exception e) {
        // use the default, fresh manager
      }
    }
    return manager;
  }

  private void writeCompilationManager(CompilationManager manager) {
    if (dependencyFile != null) {
      try {
        ObjectOutputStream oos = new ObjectOutputStream(dependencyFile.openOutputStream());
        oos.writeObject(manager);
        oos.close();
      } catch (IOException e) {
        // Fail silently. The compilation manager is only an optimization, so
        // it is ok if it gets lost
      }
    }
  }

  /**
   * Checks that all of {@code allowedOutputs} can actually be created by the
   * specified {@code CompilationUnit}s.
   *
   * @throws InvalidConfigException if impossible allowed output is found.
   */
  private static void validateAllowedOutputs(Set<FileRef> allowedOutputs,
                                             Iterable<FileRef> sourceFileRefs,
                                             Iterable<OutputLanguage> outputLanguages,
                                             long compilationVersion)
      throws InvalidConfigException {
    if (!allowedOutputs.isEmpty()) {
      Set<FileRef> possibleOutputs = computePossibleOutputs(sourceFileRefs, outputLanguages,
                                                            compilationVersion);
      List<String> impossibleOutputs = Lists.newArrayList();
      for (FileRef allowedOutput : allowedOutputs) {
        if (!possibleOutputs.contains(allowedOutput)) {
          impossibleOutputs.add(allowedOutput.toFilename());
        }
      }
      if (!impossibleOutputs.isEmpty()) {
        Collections.sort(impossibleOutputs);
        throw new InvalidConfigException(
            "The following are listed as allowed output files but are not"
            + " possible given the specified inputs: "
            + Joiner.on(", ").join(impossibleOutputs));
      }
    }
  }

  /**
   * Compute the outputs that are possible based on the input files and
   * requested output languages.
   */
  private static Set<FileRef> computePossibleOutputs(Iterable<FileRef> sourceFileRefs,
                                                     Iterable<OutputLanguage> outputLanguages,
                                                     long compilationVersion) {
    Set<FileRef> result = Sets.newHashSet();
    for (FileRef sourceFileRef : sourceFileRefs) {
      for (OutputLanguage language : outputLanguages) {
        String suffix = language.getSuffix(compilationVersion);
        FileRef outputFileRef = sourceFileRef.removeExtension().addSuffix(suffix);
        result.add(outputFileRef);
      }
    }
    return result;
  }
}
